<?php

if (!defined('BASEPATH'))
    exit('No direct script access allowed');
/**
 * IonAuth MongoDB Model
 *
 * A rewrite of IonAuth model to use MongoDB as database backend. It
 * requires both CodeIgniter MongoDB Active Record and CodeIgniter MongoDB Session
 * libraries installed.
 *
 * This model class will be loaded in case that it's set to use MongoDB as
 * database backend instead of the original model class, see controller and library
 * files for more info on its internal usage.
 *
 * @package		CodeIgniter
 * @author		Sepehr Lajevardi <me@sepehr.ws>
 * @copyright	Copyright (c) 2012 Sepehr Lajevardi.
 * @license		http://codeigniter.com/user_guide/license.html
 * @link		https://github.com/sepehr/ci-mongodb-ionauth-module
 * @version 	Version 1.0
 * @filesource
 */
// ------------------------------------------------------------------------

/**
 * IonAuth MongoDB Model
 *
 * A rewrite of IonAuth model class to use MongoDB as database backend.
 *
 * @package 	CodeIgniter
 * @subpackage	Models
 * @category	Authentication
 * @author		Sepehr Lajevardi <me@sepehr.ws>
 * @link		https://github.com/sepehr/ci-mongodb-ionauth-module
 * @todo		Re-document the code!
 */
class Ion_auth_mongodb_model extends CI_Model {

    /**
     * Holds the name of MongoDB collections
     *
     * @var array
     */
    public $collections = array();

    /**
     * activation code
     *
     * @var string
     */
    public $activation_code;

    /**
     * forgotten password key
     *
     * @var string
     */
    public $forgotten_password_code;

    /**
     * new password
     *
     * @var string
     */
    public $new_password;

    /**
     * Identity
     *
     * @var string
     */
    public $identity;

    /**
     * Where
     *
     * @var array
     */
    public $_ion_where = array();

    /**
     * Select
     *
     * @var string
     */
    public $_ion_select = array();

    /**
     * Limit
     *
     * @var string
     */
    public $_ion_limit = NULL;

    /**
     * Offset
     *
     * @var string
     */
    public $_ion_offset = NULL;

    /**
     * Order By
     *
     * @var string
     */
    public $_ion_order_by = NULL;

    /**
     * Order
     *
     * @var string
     */
    public $_ion_order = NULL;

    /**
     * Hooks
     *
     * @var object
     */
    protected $_ion_hooks;

    /**
     * Response
     *
     * @var string
     */
    protected $response = NULL;

    /**
     * message (uses lang file)
     *
     * @var string
     */
    protected $messages;

    /**
     * error message (uses lang file)
     *
     * @var string
     */
    protected $errors;

    /**
     * error start delimiter
     *
     * @var string
     */
    protected $error_start_delimiter;

    /**
     * error end delimiter
     *
     * @var string
     */
    protected $error_end_delimiter;

    /**
     * caching of users and their groups
     *
     * @var array
     */
    public $_cache_user_in_group = array();

    /**
     * caching of groups
     *
     * @var array
     */
    protected $_cache_groups = array();

    // ------------------------------------------------------------------------

    /**
     * IonAuth MongoDB Model Constructor
     */
    public function __construct() {
        parent::__construct();
        $this->load->library('mongo_db');
        $this->load->config('ion_auth', TRUE);
        $this->load->helper('cookie');
        $this->load->helper('date');

        //Load the session, CI2 as a library, CI3 uses it as a driver
        if (substr(CI_VERSION, 0, 1) == '2') {
            $this->load->library('session');
        } else {
            $this->load->driver('session');
        }

        $this->lang->load('ion_auth');

        // Initialize MongoDB collection names
        $this->collections = $this->config->item('collections', 'ion_auth');

        // Initialize general config directives
        $this->identity_column = $this->config->item('identity', 'ion_auth');
        $this->store_salt = $this->config->item('store_salt', 'ion_auth');
        $this->salt_length = $this->config->item('salt_length', 'ion_auth');

        // Initialize hash method directives (Bcrypt)
        $this->hash_method = $this->config->item('hash_method', 'ion_auth');
        $this->default_rounds = $this->config->item('default_rounds', 'ion_auth');
        $this->random_rounds = $this->config->item('random_rounds', 'ion_auth');
        $this->min_rounds = $this->config->item('min_rounds', 'ion_auth');
        $this->max_rounds = $this->config->item('max_rounds', 'ion_auth');


        // Initialize messages and errors directives
        $this->messages = array();
        $this->errors = array();
        $this->message_start_delimiter = $this->config->item('message_start_delimiter', 'ion_auth');
        $this->message_end_delimiter = $this->config->item('message_end_delimiter', 'ion_auth');
        $this->error_start_delimiter = $this->config->item('error_start_delimiter', 'ion_auth');
        $this->error_end_delimiter = $this->config->item('error_end_delimiter', 'ion_auth');

        // Initialize IonAuth hooks object
        $this->_ion_hooks = new stdClass;

        // Load the Bcrypt class if needed
        if ($this->hash_method == 'bcrypt') {
            if ($this->random_rounds) {
                $rand = rand($this->min_rounds, $this->max_rounds);
                $rounds = array('rounds' => $rand);
            } else {
                $rounds = array('rounds' => $this->default_rounds);
            }
            $this->load->library('bcrypt', $rounds);
        }

        $this->trigger_events('model_constructor');
    }

    // ------------------------------------------------------------------------

    /**
     * Hashes the password to be stored in the database.
     */
    public function hash_password($password, $salt = FALSE, $use_sha1_override = FALSE) {
        if (empty($password)) {
            return FALSE;
        }

        // Bcrypt
        if ($use_sha1_override === FALSE && $this->hash_method == 'bcrypt') {
            return $this->bcrypt->hash($password);
        }


        if ($this->store_salt && $salt) {
            return sha1($password . $salt);
        } else {
            $salt = $this->salt();
            return $salt . substr(sha1($salt . $password), 0, -$this->salt_length);
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Takes a password and validates it against an entry in the collection.
     */
    public function hash_password_db($id, $password, $use_sha1_override = FALSE) {
        if (empty($id) || empty($password)) {
            return FALSE;
        }

        $this->trigger_events('extra_where');

        $document = $this->mongo_db
                ->select(array('password', 'salt'))
                ->where('_id', new MongoId($id))
                ->limit(1)
                ->get($this->collections['users']);
        $hash_password_db = (object) $document[0];

        if (count($document) !== 1) {
            return FALSE;
        }

        // Bcrypt
        if ($use_sha1_override === FALSE && $this->hash_method == 'bcrypt') {
            if ($this->bcrypt->verify($password, $hash_password_db->password)) {
                return TRUE;
            }

            return FALSE;
        }

        // SHA1
        if ($this->store_salt) {
            $db_password = sha1($password . $hash_password_db->salt);
        } else {
            $salt = substr($hash_password_db->password, 0, $this->salt_length);
            $db_password = $salt . substr(sha1($salt . $password), 0, -$this->salt_length);
        }

        return ($db_password == $hash_password_db->password);
    }

    // ------------------------------------------------------------------------

    /**
     * Generates a random salt value for forgotten passwords or any other keys. Uses SHA1.
     */
    public function hash_code($password) {
        return $this->hash_password($password, FALSE, TRUE);
    }

    /**
     * Generates a random salt value.
     */
    public function salt() {
        return substr(md5(uniqid(rand(), true)), 0, $this->salt_length);
    }

    // ------------------------------------------------------------------------

    /**
     * Validates and removes activation code.
     */
    public function activate($id, $code = FALSE) {
        $this->trigger_events('pre_activate');

        // If activation code is set
        if ($code !== FALSE) {
            // Get identity value of the activation code
            $docs = $this->mongo_db
                    ->select($this->identity_column)
                    ->where('activation_code', $code)
                    ->limit(1)
                    ->get($this->collections['users']);
            $result = (object) $docs[0];

            // If unsuccessfull
            if (count($docs) !== 1) {
                $this->trigger_events(array('post_activate', 'post_activate_unsuccessful'));
                $this->set_error('docs');
                return FALSE;
            }

            $identity = $result->{$this->identity_column};

            $this->trigger_events('extra_where');
            $updated = $this->mongo_db
                    ->where($this->identity_column, $identity)
                    ->set(array('activation_code' => NULL, 'active' => 1))
                    ->update($this->collections['users']);
        }
        // Activation code is not set
        else {
            $this->trigger_events('extra_where');
            $updated = $this->mongo_db
                    ->where('_id', new MongoId($id))
                    ->set(array('activation_code' => NULL, 'active' => 1))
                    ->update($this->collections['users']);
        }


        if ($updated) {
            $this->trigger_events(array('post_activate', 'post_activate_successful'));
            $this->set_message('activate_successful');
        } else {
            $this->trigger_events(array('post_activate', 'post_activate_unsuccessful'));
            $this->set_error('activate_unsuccessful');
        }

        return $updated;
    }

    // ------------------------------------------------------------------------

    /**
     * Updates a user document with an activation code.
     */
    public function deactivate($id = NULL) {
        $this->trigger_events('deactivate');

        if (!isset($id)) {
            $this->set_error('deactivate_unsuccessful');
            return FALSE;
        }

        $activation_code = sha1(md5(microtime()));
        $this->activation_code = $activation_code;
        $this->trigger_events('extra_where');

        $updated = $this->mongo_db
                ->where('_id', new MongoId($id))
                ->set(array('activation_code' => $activation_code, 'active' => 0))
                ->update($this->collections['users']);

        if ($updated) {
            $this->set_message('deactivate_successful');
        } else {
            $this->set_error('deactivate_unsuccessful');
        }

        return $updated;
    }

    // ------------------------------------------------------------------------

    /**
     * Clears forgotten password code from database.
     */
    public function clear_forgotten_password_code($code) {
        if (empty($code)) {
            return FALSE;
        }

        $count = count($this->mongo_db
                        ->where('forgotten_password_code', $code)
                        ->get($this->collections['users']));

        if ($count > 0) {
            $this->mongo_db
                    ->where('forgotten_password_code', $code)
                    ->set(array('forgotten_password_code' => NULL, 'forgotten_password_time' => NULL))
                    ->update($this->collections['users']);

            return TRUE;
        }
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Resets password.
     *
     * @return bool
     */
    public function reset_password($identity, $new) {
        $this->trigger_events('pre_change_password');

        if (!$this->identity_check($identity)) {
            $this->trigger_events(array('post_change_password', 'post_change_password_unsuccessful'));
            return FALSE;
        }

        $this->trigger_events('extra_where');

        $docs = $this->mongo_db
                ->select(array('_id', 'password', 'salt'))
                ->where($this->identity_column, $identity)
                ->limit(1)
                ->get($this->collections['users']);

        // Unsuccessfull password change
        if (count($docs) !== 1) {
            $this->trigger_events(array('post_change_password', 'post_change_password_unsuccessful'));
            $this->set_error('password_change_unsuccessful');
            return FALSE;
        }

        // Generate new password hash
        $result = (object) $docs[0];
        $new = $this->hash_password($new, $result->salt);

        $this->trigger_events('extra_where');

        // Store the new password and reset the remember code so all remembered instances have
        // to re-login. Also clear the forgotten password code.
        $updated = $this->mongo_db
                ->where($this->identity_column, $identity)
                ->set(array(
                    'password' => $new,
                    'remember_code' => NULL,
                    'forgotten_password_code' => NULL,
                    'forgotten_password_time' => NULL,
                ))
                ->update($this->collections['users']);

        if ($updated) {
            $this->trigger_events(array('post_change_password', 'post_change_password_successful'));
            $this->set_message('password_change_successful');
        } else {
            $this->trigger_events(array('post_change_password', 'post_change_password_unsuccessful'));
            $this->set_error('password_change_unsuccessful');
        }

        return $updated;
    }

    // ------------------------------------------------------------------------

    /**
     * Changes password.
     *
     * @return bool
     */
    public function change_password($identity, $old, $new) {
        $this->trigger_events('pre_change_password');
        $this->trigger_events('extra_where');

        $docs = $this->mongo_db
                ->select(array('_id', 'password', 'salt'))
                ->where($this->identity_column, $identity)
                ->limit(1)
                ->get($this->collections['users']);

        if (count($docs) !== 1) {
            $this->trigger_events(array('post_change_password', 'post_change_password_unsuccessful'));
            $this->set_error('password_change_unsuccessful');
            return FALSE;
        }

        $result = (object) $docs[0];
        $db_password = $result->password;
        $old = $this->hash_password_db($result->_id, $old);
        $new = $this->hash_password($new, $result->salt);

        if ($old === TRUE) {
            $this->trigger_events('extra_where');

            // Store the new password and reset the remember code so all remembered instances have to re-login
            $updated = $this->mongo_db
                    ->where($this->identity_column, $identity)
                    ->set(array('password' => $new, 'remember_code' => NULL))
                    ->update($this->collections['users']);

            if ($updated) {
                $this->trigger_events(array('post_change_password', 'post_change_password_successful'));
                $this->set_message('password_change_successful');
            } else {
                $this->trigger_events(array('post_change_password', 'post_change_password_unsuccessful'));
                $this->set_error('password_change_unsuccessful');
            }

            return $updated;
        }

        $this->set_error('password_change_unsuccessful');
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Checks username.
     *
     * @return bool
     */
    public function username_check($username = '') {
        $this->trigger_events('username_check');

        if (empty($username)) {
            return FALSE;
        }

        $this->trigger_events('extra_where');
        $username = new MongoRegex('/^' . $username . '$/i');
        return count($this->mongo_db
                                ->where('username', $username)
                                ->get($this->collections['users'])) > 0;
    }

    // ------------------------------------------------------------------------

    /**
     * Checks email.
     *
     * @return bool
     */
    public function email_check($email = '') {
        $this->trigger_events('email_check');

        if (empty($email)) {
            return FALSE;
        }

        $this->trigger_events('extra_where');
        $email = new MongoRegex('/^' . $email . '$/i');
        return count($this->mongo_db
                                ->where('email', $email)
                                ->get($this->collections['users'])) > 0;
    }

    // ------------------------------------------------------------------------

    /**
     * Checks identity field.
     *
     * @return bool
     */
    protected function identity_check($identity = '') {
        $this->trigger_events('identity_check');

        if (empty($identity)) {
            return FALSE;
        }

        return count($this->mongo_db
                                ->where($this->identity_column, $identity)
                                ->get($this->collections['users'])) > 0;
    }

    // ------------------------------------------------------------------------

    /**
     * Inserts a forgotten password key.
     *
     * @return bool
     */
    public function forgotten_password($identity) {
        if (empty($identity)) {
            $this->trigger_events(array('post_forgotten_password', 'post_forgotten_password_unsuccessful'));
            return FALSE;
        }

        $this->forgotten_password_code = $this->hash_code(microtime() . $identity);
        $this->trigger_events('extra_where');

        $updated = $this->mongo_db
                ->where($this->identity_column, $identity)
                ->set(array(
                    'forgotten_password_code' => $this->forgotten_password_code,
                    'forgotten_password_time' => time()
                ))
                ->update($this->collections['users']);

        if ($updated) {
            $this->trigger_events(array('post_forgotten_password', 'post_forgotten_password_successful'));
        } else {
            $this->trigger_events(array('post_forgotten_password', 'post_forgotten_password_unsuccessful'));
        }

        return $updated;
    }

    // ------------------------------------------------------------------------

    /**
     * Completes a forgotten password procedure.
     *
     * @return string
     */
    public function forgotten_password_complete($code, $salt = FALSE) {
        $this->trigger_events('pre_forgotten_password_complete');

        if (empty($code)) {
            $this->trigger_events(array(
                'post_forgotten_password_complete',
                'post_forgotten_password_complete_unsuccessful',
            ));
            return FALSE;
        }

        // Get user document for this forgotten password code
        $profile = $this->where('forgotten_password_code', $code)->users()->document();

        // If there's any user with this code:
        if ($profile) {
            // Check if the forgot password request is not expired yet
            if ($this->config->item('forgot_password_expiration', 'ion_auth') > 0) {
                $expiration = $this->config->item('forgot_password_expiration', 'ion_auth');

                // If the forgot password request is expired, abort the operation!
                if (time() - $profile->forgotten_password_time > $expiration) {
                    $this->set_error('forgot_password_expired');
                    $this->trigger_events(array(
                        'post_forgotten_password_complete',
                        'post_forgotten_password_complete_unsuccessful'
                    ));
                    return FALSE;
                }
            }

            // Update the user document with the new password
            $password = $this->salt();
            $this->mongo_db
                    ->where('forgotten_password_code', $code)
                    ->set(array(
                        'password' => $this->hash_password($password, $salt),
                        'forgotten_password_code' => NULL,
                        'active' => 1,
                    ))
                    ->update($this->collections['users']);

            // Trigger appropriate hooks
            $this->trigger_events(array(
                'post_forgotten_password_complete',
                'post_forgotten_password_complete_successful'
            ));

            // And return the password
            return $password;
        }

        // But if there were no users with that forgotten password code:
        $this->trigger_events(array(
            'post_forgotten_password_complete',
            'post_forgotten_password_complete_unsuccessful'
        ));
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Inserts a user document into users collection.
     *
     * @return bool
     */
    public function register($username, $password, $email, $additional_data = array(), $groups = array()) {
        $this->trigger_events('pre_register');
        $manual_activation = $this->config->item('manual_activation', 'ion_auth');

        // Check if email already exists
        if ($this->identity_column == 'email' && $this->email_check($email)) {
            $this->set_error('account_creation_duplicate_email');
            return FALSE;
        }
        // Check if username already exists
        elseif ($this->identity_column == 'username' && $this->username_check($username)) {
            $this->set_error('account_creation_duplicate_username');
            return FALSE;
        }

        // If username is taken, use username1 or username2, etc.
        // TODO: Drop this shit!
        if ($this->identity_column != 'username') {
            $original_username = $username;
            for ($i = 0; $this->username_check($username); $i++) {
                if ($i > 0) {
                    $username = $original_username . $i;
                }
            }
        }

        // IP address
        $ip_address = $this->_prepare_ip($this->input->ip_address());
        $salt = $this->store_salt ? $this->salt() : FALSE;
        $password = $this->hash_password($password, $salt);

        // New user document
        $data = array(
            'username' => $username,
            'password' => $password,
            'email' => $email,
            'ip_address' => $ip_address,
            'created_on' => time(),
            'last_login' => time(),
            'active' => ($manual_activation === FALSE ? 1 : 0),
            'groups' => array(),
        );

        // Store salt in document?
        if ($this->store_salt) {
            $data['salt'] = $salt;
        }

        // Add groups to the user document, We don't use
        // add_to_group() API function here regarding lesser queries.
        if (!empty($groups)) {
            foreach ($groups as $group) {
                $data['groups'][] = $group;
            }
        }
        // Add to default group if not already set,
        // get the ID of the default group first:
        $default_group = $this->where('name', $this->config->item('default_group', 'ion_auth'))->group()->document();
        if ((isset($default_group->_id) && !isset($groups)) || (empty($groups) && !in_array($default_group->id, $groups))) {
            $data['groups'][] = $default_group->_id;
        }

        // Filter out any data passed that doesn't have a matching column in the
        // user document and merge the set user data with the passed additional data
        $data = array_merge($this->_filter_data($this->collections['users'], $additional_data), $data);

        $this->trigger_events('extra_set');

        // Insert new document and store the _id value
        $id = $this->mongo_db->insert($this->collections['users'], $data);

        $this->trigger_events('post_register');

        // Return new document _id or FALSE on failure
        return isset($id) ? $id : FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Checks credentials and logs the passed user in if possible.
     *
     * @return bool
     */
    public function login($identity, $password, $remember = FALSE) {
        $this->trigger_events('pre_login');

        if (empty($identity) || empty($password)) {
            $this->set_error('login_unsuccessful');
            return FALSE;
        }

        $this->trigger_events('extra_where');

        $document = $this->mongo_db
                ->select(array($this->identity_column, '_id', 'username', 'email', 'password', 'active', 'last_login'))
                // MongoDB is vulnerable to SQL Injection like attacks (in PHP at least), in MongoDB
                // PHP driver we use objects to make queries and as we know PHP allows us to submit
                // objects via GET, POST, etc. and so getting user input like password[$ne]=1 is possible
                // which translates to: array('$ne' => 1) in for example find queries. So we make sure that
                // what we put into a collection is strictly string typed. We also watch what we put in our
                // stomach, goveg! :))
                ->where($this->identity_column, (string) $identity)
                ->limit(1)
                ->get($this->collections['users']);

        // If user document found
        if (count($document) === 1) {
            $user = (object) $document[0];
            $password = $this->hash_password_db($user->_id, $password);

            if ($password === TRUE) {
                // Not yet activated?
                if ($user->active == 0) {
                    $this->trigger_events('post_login_unsuccessful');
                    $this->set_error('login_unsuccessful_not_active');
                    return FALSE;
                }

                // Set user session data
                $session_data = array(
                    'identity' => $user->{$this->identity_column},
                    'username' => $user->username,
                    'email' => $user->email,
                    'user_id' => $user->_id,
                    'old_last_login' => $user->last_login
                );
                $this->session->set_userdata($session_data);

                // Clean login attempts, also update last login time
                $this->update_last_login($user->_id);
                $this->clear_login_attempts($identity);

                // Check whether we should remember the user
                if ($remember && $this->config->item('remember_users', 'ion_auth')) {
                    $this->remember_user($user->_id);
                }

                $this->trigger_events(array('post_login', 'post_login_successful'));
                $this->set_message('login_successful');

                return TRUE;
            }
        }

        // The user document was not found
        $this->hash_password($password);
        $this->increase_login_attempts($identity);
        $this->trigger_events('post_login_unsuccessful');
        $this->set_error('login_unsuccessful');

        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Checks whether the maximum login attempts limit is reached.
     *
     * @return bool
     */
    public function is_max_login_attempts_exceeded($identity) {
        // Do we set to track login attempts?
        if ($this->config->item('track_login_attempts', 'ion_auth')) {
            $max_attempts = $this->config->item('maximum_login_attempts', 'ion_auth');

            if ($max_attempts > 0) {
                $attempts = $this->get_attempts_num($identity);
                return $attempts >= $max_attempts;
            }
        }
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Returns the number of attempts to login occured from given IP or identity
     *
     * @return int
     */
    function get_attempts_num($identity) {
        // Do we set to track login attempts?
        if ($this->config->item('track_login_attempts', 'ion_auth')) {
            $this->mongo_db->where('ip_address', $this->_prepare_ip($this->input->ip_address()));

            if (strlen($identity) > 0) {
                $this->mongo_db->or_where('login', $identity);
            }

            $document = $this->mongo_db->get($this->collections['login_attempts']);
            return count($document);
        }
        return 0;
    }

    // ------------------------------------------------------------------------

    /**
     * Increses login attempts of the passed user
     */
    public function increase_login_attempts($identity) {
        // Do we set to track login attempts?
        if ($this->config->item('track_login_attempts', 'ion_auth')) {
            return $this->mongo_db->insert($this->collections['login_attempts'], array(
                        'ip_address' => $this->_prepare_ip($this->input->ip_address()),
                        'login' => (string) $identity,
                        'time' => time(),
            ));
        }
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Clears login attempts of the passed identity.
     */
    public function clear_login_attempts($identity, $expire_period = 86400) {
        // Do we set to track login attempts?
        if ($this->config->item('track_login_attempts', 'ion_auth')) {
            return $this->mongo_db
                            ->where(array(
                                'login' => $identity,
                                'ip_address' => $this->_prepare_ip($this->input->ip_address())
                            ))
                            // Purge obsolete login attempts
                            ->or_where(array('time <' => time() - $expire_period))
                            ->delete($this->collections['login_attempts']);
        }
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Sets query limit parameter.
     */
    public function limit($limit) {
        $this->trigger_events('limit');
        $this->_ion_limit = $limit;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Sets query offset parameter.
     */
    public function offset($offset) {
        $this->trigger_events('offset');
        $this->_ion_offset = $offset;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Sets query where conditions.
     */
    public function where($where, $value = NULL) {
        $this->trigger_events('where');

        if (!is_array($where)) {
            $where = array($where => $value);
        }

        array_push($this->_ion_where, $where);

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Sets query select portion.
     */
    public function select($select) {
        $this->trigger_events('select');

        $this->_ion_select[] = $select;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Sets query orderby parameter.
     */
    public function order_by($by, $order = 'desc') {
        $this->trigger_events('order_by');

        $this->_ion_order_by = $by;
        $this->_ion_order = $order;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Returns a single document object from an array of results.
     *
     * It's our MongoDB equivalent of CodeIgniter row() method.
     *
     * @return object
     */
    public function document() {
        $this->trigger_events('document');

        $document = array();
        if (isset($this->response[0])) {
            // Clone mongoid of the resulting array, if necessary
            $this->response[0] = $this->_clone_mongoid($this->response[0]);
            // Get the first result as an object
            $document = (object) $this->response[0];
        }
        // Free memory
        unset($this->response);

        return $document;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Returns a single document object from an array of results.
     *
     * It's our MongoDB equivalent of CodeIgniter row_array() method.
     *
     * @return array
     */
    public function document_array() {
        $this->trigger_events(array('document', 'document_array'));

        $document = array();
        if (isset($this->response[0])) {
            // Clone mongoid of the resulting array, if necessary
            $this->response[0] = $this->_clone_mongoid($this->response[0]);
            // Get the first result as an array
            $document = $this->response[0];
        }
        // Free memory
        unset($this->response);

        return $document;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Alias function for document() to maintain API consistency.
     *
     * @return object
     */
    public function row() {
        return $this->document();
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Alias function for document_array() to maintain API consistency.
     *
     * @return object
     */
    public function row_array() {
        return $this->document_array();
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Returns query results as an array of objects.
     *
     * This helper, and a few others (result_array, row, row_array) are
     * implemented to mimic the behavior of their equivalent functions in the
     * original model, but in MongoDB interface environment.
     *
     * TODO: These kinda helpers should exist in MongoDB Active Record Library.
     */
    public function result() {
        $this->trigger_events('result');

        if (!empty($this->response)) {
            foreach ($this->response as $key => $value) {
                // We need to add an arbitrary "id" field to the resulted
                // object to maintain IonAuth compatibility with both
                // mongodb library and the native database drivers with
                // minimum level of code change.
                $this->response[$key] = $this->_clone_mongoid($value);

                // Typecast document results into objects for API consistency
                $this->response[$key] = (object) $this->response[$key];
            }
        }

        $result = $this->response;
        unset($this->response);

        return $result;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Returns query results as an array of arrays.
     */
    public function result_array() {
        $this->trigger_events(array('result', 'result_array'));

        $result = $this->response;
        unset($this->response);

        // We need to add an arbitrary "id" field to the resulted
        // object to maintain IonAuth compatibility with both
        // mongodb library and the native database drivers with
        // minimum level of code change.
        $result = $this->_clone_mongoid($result);

        return $result;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Builds & executes a MongoDB query based on the already set parameters
     * against users collection.
     *
     * @return object
     */
    public function users($groups = NULL) {
        $this->trigger_events('users');

        // If there are specific select fields, apply them and flush SELECT buffer
        if (isset($this->_ion_select)) {
            foreach ($this->_ion_select as $select) {
                $this->mongo_db->select($select);
            }
            $this->_ion_select = array();
        }

        // Filter by group if any passed
        if (isset($groups)) {
            // Build an array if only one group was passed
            if (is_numeric($groups)) {
                $groups = array($groups);
            }

            if (!empty($groups)) {
                $this->mongo_db->where_in('groups', $groups);
            }
        }

        $this->trigger_events('extra_where');

        // Run each set where conditions and flush conditions buffer
        if (isset($this->_ion_where)) {
            foreach ($this->_ion_where as $where) {
                $this->mongo_db->where($where);
            }
            $this->_ion_where = array();
        }

        // Apply limit portion of the query if set
        if (isset($this->_ion_limit) && isset($this->_ion_offset)) {
            $this->mongo_db
                    ->limit($this->_ion_limit)
                    ->offset($this->_ion_offset);
            // Flush...
            $this->_ion_limit = NULL;
            $this->_ion_offset = NULL;
        }
        // Limit only?
        elseif (isset($this->_ion_limit)) {
            $this->mongo_db->limit($this->_ion_limit);
            $this->_ion_limit = NULL;
        }

        // Finally apply the order portion
        if (isset($this->_ion_order_by) && isset($this->_ion_order)) {
            $this->mongo_db->order_by(array($this->_ion_order_by => $this->_ion_order));
            // Flush order buffers
            $this->_ion_order = NULL;
            $this->_ion_order_by = NULL;
        }

        // Execute and return the object itself for the sake of chaining!
        $this->response = $this->mongo_db->get($this->collections['users']);
        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Returns user object by its passed ID.
     *
     * @return object
     */
    public function user($id = FALSE) {
        $this->trigger_events('user');

        // If no id was passed use the current user id stored in session
        $id || $id = $this->session->userdata('user_id');

        // Set query parameters
        $this->limit(1);
        $this->where('_id', new MongoId($id));

        // Build and execute the query
        $this->users();

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Returns an array of the user groups.
     *
     * @return array
     */
    public function get_users_groups($id = FALSE) {
        $this->trigger_events('get_users_group');

        // If no id passed use the current user id stored in session
        $id || $id = $this->session->userdata('user_id');

        $groups = array();
        // Load user's group IDs array
        $user = $this->mongo_db
                ->select(array('groups'))
                ->where('_id', new MongoId($id))
                ->limit(1)
                ->get($this->collections['users']);

        if (empty($user)) {
            $this->response = new stdClass;
            return $this;
        }

        // Buildup user groups data array
        $user = (object) $user[0];
        foreach ($user->groups as $group_id) {
            $groups[] = $this->group($group_id)->document();
        }

        $this->response = $groups;
        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Adds group ID to the specified user document.
     *
     * @return bool
     */
    public function add_to_group($group_id, $user_id = FALSE) {
        $this->trigger_events('add_to_group');

        // If no id passed use the current user id stored in session
        $user_id || $user_id = $this->session->userdata('user_id');

        return $this->mongo_db
                        ->where('_id', new MongoId($user_id))
                        ->push('groups', $group_id)
                        ->update($this->collections['users']);
    }

    // ------------------------------------------------------------------------

    /**
     * Removes passed group from the user document.
     *
     * If the group ID is not set, it will remove all groups
     * from the user document.
     *
     * @return bool
     */
    public function remove_from_group($group_name = FALSE, $user_id = FALSE) {
        $this->trigger_events('remove_from_group');

        // If no id passed use the current user id stored in session
        $user_id || $user_id = $this->session->userdata('user_id');

        // If no group name is passed remove user from all groups
        if (empty($group_id)) {
            return $this->mongo_db
                            ->where('_id', new MongoId($user_id))
                            ->set('groups', array())
                            ->update($this->collections['users']);
        }
        // Only remove the specified group name from the user document
        else {
            return $this->mongo_db
                            ->where('_id', new MongoId($user_id))
                            ->pull('groups', array($group_id))
                            ->update($this->collections['users']);
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Builds and executes a MongoDB query against groups collection.
     *
     * @return object
     */
    public function groups() {
        $this->trigger_events('groups');

        // Apply buffered conditions, and flush immediately
        if (isset($this->_ion_where)) {
            foreach ($this->_ion_where as $where) {
                $this->mongo_db->where($where);
            }
            $this->_ion_where = array();
        }

        // Apply limit/offset portions, and flush immediately
        if (isset($this->_ion_limit) && isset($this->_ion_offset)) {
            $this->mongo_db
                    ->limit($this->_ion_limit)
                    ->offset($this->_ion_offset);
            // Flush...
            $this->_ion_limit = NULL;
            $this->_ion_offset = NULL;
        }
        // Limit only?
        elseif (isset($this->_ion_limit)) {
            $this->mongo_db->limit($this->_ion_limit);
            $this->_ion_limit = NULL;
        }

        // Apply order portion of query
        if (isset($this->_ion_order_by) && isset($this->_ion_order)) {
            $this->mongo_db->order_by(array($this->_ion_order_by => $this->_ion_order));
        }

        // Execute, store results and return the object itself
        $this->response = $this->mongo_db->get($this->collections['groups']);
        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Returns a group object based on pre-defined buffered parameters.
     *
     * @return object
     */
    public function group($id = NULL) {
        $this->trigger_events('group');

        if (isset($id)) {
            $this->mongo_db->where('_id', new MongoId($id));
        }

        // Set query parameters
        $this->limit(1);

        // Execute and return results
        return $this->groups();
    }

    // ------------------------------------------------------------------------

    /**
     * Updates a user document.
     *
     * @return bool
     */
    public function update($id, array $data) {
        $this->trigger_events('pre_update_user');

        // Get user document to update
        $user = $this->user($id)->document();

        // If we're updating user document with a new identity
        // and the identity is not available to register, bam!
        if (array_key_exists($this->identity_column, $data) &&
                $this->identity_check($data[$this->identity_column]) &&
                $user->{$this->identity_column} !== $data[$this->identity_column]) {
            $this->set_error('account_creation_duplicate_' . $this->identity_column);
            $this->trigger_events(array('post_update_user', 'post_update_user_unsuccessful'));
            $this->set_error('update_unsuccessful');

            return FALSE;
        }

        // Filter the data passed
        $data = $this->_filter_data($this->collections['users'], $data);

        // Hash new password
        if (array_key_exists('password', $data)) {
            if (!empty($data['password'])) {
                $data['password'] = $this->hash_password($data['password'], $user->salt);
            } else {
                // unset password so it doesn't effect database entry if password field empty
                unset($data['password']);
            }
        }

        // TODO: DO WE NEED TO CHECK EMAIL AND USERNAME VALUES REGARDLESS
        // OF WHAT IDENTITY FIELD IS? ARE THEY STILL UNIQUE? DONNO!
        // Check if new email already exists
        if ($this->identity_column !== 'email' && array_key_exists('email', $data) &&
                $this->email_check($data['email']) && $user->email !== $data['email']) {
            $this->set_error('account_creation_duplicate_email');
            return FALSE;
        }

        // Check if new username already exists
        if ($this->identity_column !== 'username' && array_key_exists('username', $data) &&
                $this->username_check($data['username']) && $user->username !== $data['username']) {
            $this->set_error('account_creation_duplicate_username');
            return FALSE;
        }

        $this->trigger_events('extra_where');

        $updated = $this->mongo_db
                ->where('_id', new MongoId($user->id))
                ->set($data)
                ->update($this->collections['users']);

        if (!$updated) {
            $this->trigger_events(array('post_update_user', 'post_update_user_unsuccessful'));
            $this->set_error('update_unsuccessful');
            return FALSE;
        }

        $this->trigger_events(array('post_update_user', 'post_update_user_successful'));
        $this->set_message('update_successful');
        return TRUE;
    }

    // ------------------------------------------------------------------------

    /**
     * Deletes a user document by its ID.
     *
     * @return bool
     */
    public function delete_user($id) {
        $this->trigger_events('pre_delete_user');

        // Delete user document (groups association will also be deleted)
        $deleted = $this->mongo_db
                ->where('_id', new MongoId($id))
                ->delete($this->collections['users']);

        if (!$deleted) {
            $this->trigger_events(array('post_delete_user', 'post_delete_user_unsuccessful'));
            $this->set_error('delete_unsuccessful');
            return FALSE;
        }

        $this->trigger_events(array('post_delete_user', 'post_delete_user_successful'));
        $this->set_message('delete_successful');
        return TRUE;
    }

    // ------------------------------------------------------------------------

    /**
     * Updates user last login timestamp.
     *
     * @return bool
     */
    public function update_last_login($id) {
        $this->load->helper('date');

        $this->trigger_events('update_last_login');
        $this->trigger_events('extra_where');

        return $this->mongo_db
                        ->where('_id', new MongoId($id))
                        ->set('last_login', time())
                        ->update($this->collections['users']);
    }

    // ------------------------------------------------------------------------

    /**
     * Sets language cookie.
     *
     * @return bool
     */
    public function set_lang($lang = 'en') {
        $this->trigger_events('set_lang');

        // if the user_expire is set to zero we'll set the expiration two years from now.
        if ($this->config->item('user_expire', 'ion_auth') === 0) {
            $expire = (60 * 60 * 24 * 365 * 2);
        }
        // otherwise use what is set
        else {
            $expire = $this->config->item('user_expire', 'ion_auth');
        }

        set_cookie(array(
            'name' => 'lang_code',
            'value' => $lang,
            'expire' => $expire
        ));

        return TRUE;
    }

    // ------------------------------------------------------------------------

    /**
     * Remembers user by setting required cookies
     *
     * @return bool
     */
    public function remember_user($id) {
        $this->trigger_events('pre_remember_user');

        if (!$id) {
            return FALSE;
        }

        // Load user document
        $user = $this->user($id)->document();
        // Re-hash user password as remember code
        $salt = sha1($user->password);

        $updated = $this->mongo_db
                ->where('_id', new MongoId($id))
                ->set('remember_code', $salt)
                ->update($this->collections['users']);

        // Set cookies
        if ($updated) {
            // if the user_expire is set to zero we'll set the expiration two years from now.
            if ($this->config->item('user_expire', 'ion_auth') === 0) {
                $expire = (60 * 60 * 24 * 365 * 2);
            }
            // otherwise use what is set
            else {
                $expire = $this->config->item('user_expire', 'ion_auth');
            }

            set_cookie(array(
                'name' => 'identity',
                'value' => $user->{$this->identity_column},
                'expire' => $expire
            ));

            set_cookie(array(
                'name' => 'remember_code',
                'value' => $salt,
                'expire' => $expire
            ));

            $this->trigger_events(array('post_remember_user', 'remember_user_successful'));
            return TRUE;
        }

        // User not found
        $this->trigger_events(array('post_remember_user', 'remember_user_unsuccessful'));
        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     * Logs in a remembered user.
     *
     * @return bool
     */
    public function login_remembered_user() {
        $this->trigger_events('pre_login_remembered_user');

        // Check for valid data
        if (!get_cookie('identity') || !get_cookie('remember_code') || !$this->identity_check(get_cookie('identity'))) {
            $this->trigger_events(array('post_login_remembered_user', 'post_login_remembered_user_unsuccessful'));
            return FALSE;
        }

        // Load the user by cookie data
        $this->trigger_events('extra_where');
        $document = $this->mongo_db
                ->select(array('_id', $this->identity_column))
                ->where($this->identity_column, get_cookie('identity'))
                ->where('remember_code', get_cookie('remember_code'))
                ->limit(1)
                ->get($this->collections['users']);

        // If the user was found, sign them in
        if (count($document)) {
            $user = (object) $document[0];

            // Update last login timestamp
            $this->update_last_login($user->id);

            // And set user session data
            $this->session->set_userdata(array(
                $this->identity_column => $user->{$this->identity_column},
                'user_id' => $user->_id,
            ));

            // Extend the users cookies if the option is enabled
            if ($this->config->item('user_extend_on_login', 'ion_auth')) {
                $this->remember_user($user->_id);
            }

            $this->trigger_events(array('post_login_remembered_user', 'post_login_remembered_user_successful'));
            return TRUE;
        }

        // User not found
        $this->trigger_events(array('post_login_remembered_user', 'post_login_remembered_user_unsuccessful'));
        return FALSE;
    }

    /**
     * create_group
     *
     * @author Ben Edmunds
     */
    public function create_group($group_name = FALSE, $group_description = '', $additional_data = array()) {
        // bail if the group name was not passed
        if (!$group_name) {
            $this->set_error('group_name_required');
            return FALSE;
        }

        // bail if the group name already exists
        $existing_group = $this->where('name', $group_name)->group()->document();
        if (isset($existing_group) && !empty($existing_group)) {
            $this->set_error('group_already_exists');
            return FALSE;
        }

        $data = array('name' => $group_name, 'description' => $group_description);

        //filter out any data passed that doesnt have a matching column in the groups table
        //and merge the set group data and the additional data
        if (!empty($additional_data))
            $data = array_merge($this->_filter_data($this->collections['groups'], $additional_data), $data);

        $this->trigger_events('extra_group_set');

        // insert the new group
        $group_id = $this->mongo_db->insert($this->collections['groups'], $data);

        // report success
        $this->set_message('group_creation_successful');

        // return the brand new group id
        return $group_id;
    }

    /**
     * update_group
     *
     * @return bool
     * @author Ben Edmunds
     * */
    public function update_group($group_id = FALSE, $group_name = FALSE, $additional_data = array()) {
        if (empty($group_id))
            return FALSE;

        $data = array();

        if (!empty($group_name)) {
            // we are changing the name, so do some checks
            // bail if the group name already exists
            $existing_group = $this->where('name', $group_name)->group()->document();
            if (isset($existing_group->id) && $existing_group->id != $group_id) {
                $this->set_error('group_already_exists');
                return FALSE;
            }

            $data['name'] = $group_name;
        }


        // IMPORTANT!! Third parameter was string type $description; this following code is to maintain backward compatibility
        // New projects should work with 3rd param as array
        if (is_string($additional_data))
            $additional_data = array('description' => $additional_data);


        //filter out any data passed that doesnt have a matching column in the groups table
        //and merge the set group data and the additional data
        if (!empty($additional_data))
            $data = array_merge($this->_filter_data($this->collections['groups'], $additional_data), $data);


        $updated = $this->mongo_db
                ->where('_id', new MongoId($group_id))
                ->set($data)
                ->update($this->collections['groups']);

        $this->set_message('group_update_successful');

        return TRUE;
    }

    /**
     * delete_group
     *
     * @return bool
     * @author Ben Edmunds
     * */
    public function delete_group($group_id = FALSE) {
        // bail if mandatory param not set
        if (!$group_id || empty($group_id)) {
            return FALSE;
        }

        $this->trigger_events('pre_delete_group');


        // delete this group
        $deleted = $this->mongo_db
                ->where('_id', new MongoId($group_id))
                ->delete($this->collections['groups']);

        if (!$deleted) {
            $this->trigger_events(array('post_delete_group', 'post_delete_group_unsuccessful'));
            $this->set_error('group_delete_unsuccessful');
            return FALSE;
        }


        $this->trigger_events(array('post_delete_group', 'post_delete_group_successful'));
        $this->set_message('group_delete_successful');
        return TRUE;
    }

    // ------------------------------------------------------------------------

    /**
     * Registers a hook.
     */
    public function set_hook($event, $name, $class, $method, $arguments) {
        $this->_ion_hooks->{$event}[$name] = (object) array(
                    'class' => $class,
                    'method' => $method,
                    'arguments' => $arguments,
        );
    }

    // ------------------------------------------------------------------------

    /**
     * Unregisters a hook.
     */
    public function remove_hook($event, $name) {
        if (isset($this->_ion_hooks->{$event}[$name])) {
            unset($this->_ion_hooks->{$event}[$name]);
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Unregisters all hooks from the passed event.
     */
    public function remove_hooks($event) {
        if (isset($this->_ion_hooks->$event)) {
            unset($this->_ion_hooks->$event);
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Calls a registered hook callback.
     */
    protected function _call_hook($event, $name) {
        if (isset($this->_ion_hooks->{$event}[$name]) &&
                method_exists($this->_ion_hooks->{$event}[$name]->class, $this->_ion_hooks->{$event}[$name]->method)) {
            $hook = $this->_ion_hooks->{$event}[$name];
            return call_user_func_array(array($hook->class, $hook->method), $hook->arguments);
        }

        return FALSE;
    }

    // ------------------------------------------------------------------------

    /**
     *
     */
    public function trigger_events($events) {
        // If it's an array, call hooks foreach event
        if (is_array($events) && !empty($events)) {
            foreach ($events as $event) {
                $this->trigger_events($event);
            }
        } else {
            if (isset($this->_ion_hooks->$events) && !empty($this->_ion_hooks->$events)) {
                foreach ($this->_ion_hooks->$events as $name => $hook) {
                    $this->_call_hook($events, $name);
                }
            }
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Sets the message delimiters
     *
     * @return bool
     */
    public function set_message_delimiters($start_delimiter, $end_delimiter) {
        $this->message_start_delimiter = $start_delimiter;
        $this->message_end_delimiter = $end_delimiter;
        return TRUE;
    }

    // ------------------------------------------------------------------------

    /**
     * Sets the error delimiters
     *
     * @return bool
     */
    public function set_error_delimiters($start_delimiter, $end_delimiter) {
        $this->error_start_delimiter = $start_delimiter;
        $this->error_end_delimiter = $end_delimiter;
        return TRUE;
    }

    // ------------------------------------------------------------------------

    /**
     * Sets a message
     */
    public function set_message($message) {
        $this->messages[] = $message;
        return $message;
    }

    // ------------------------------------------------------------------------

    /**
     * Applies delimiters and returns themed messages
     */
    public function messages() {
        $_output = '';
        foreach ($this->messages as $message) {
            $message_lang = $this->lang->line($message) ? $this->lang->line($message) : '##' . $message . '##';
            $_output .= $this->message_start_delimiter . $message_lang . $this->message_end_delimiter;
        }

        return $_output;
    }

    // ------------------------------------------------------------------------

    /**
     * Return messages as an array, langified or not
     * */
    public function messages_array($langify = TRUE) {
        if ($langify) {
            $_output = array();
            foreach ($this->messages as $message) {
                $messageLang = $this->lang->line($message) ? $this->lang->line($message) : '##' . $message . '##';
                $_output[] = $this->message_start_delimiter . $messageLang . $this->message_end_delimiter;
            }
            return $_output;
        } else {
            return $this->messages;
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Sets an error message
     */
    public function set_error($error) {
        $this->errors[] = $error;
        return $error;
    }

    // ------------------------------------------------------------------------

    /**
     * Applies delimiters and returns themed errors
     */
    public function errors() {
        $_output = '';
        foreach ($this->errors as $error) {
            $error_lang = $this->lang->line($error) ? $this->lang->line($error) : '##' . $error . '##';
            $_output .= $this->error_start_delimiter . $error_lang . $this->error_end_delimiter;
        }

        return $_output;
    }

    // ------------------------------------------------------------------------

    /**
     * Return errors as an array, langified or not
     * */
    public function errors_array($langify = TRUE) {
        if ($langify) {
            $_output = array();
            foreach ($this->errors as $error) {
                $errorLang = $this->lang->line($error) ? $this->lang->line($error) : '##' . $error . '##';
                $_output[] = $this->error_start_delimiter . $errorLang . $this->error_end_delimiter;
            }
            return $_output;
        } else {
            return $this->errors;
        }
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Filters out any data passed that doesn't have a matching column in $table.
     *
     * Since MongoDB is a schemaless database, we define collection fields as an static array,
     * why we do so? MongoDB is vulnerable to Null Byte Injection as stated in the below article.
     *
     * @see http://www.idontplaydarts.com/2011/02/mongodb-null-byte-injection-attacks/
     * @return array
     */
    protected function _filter_data($collection, $data) {
        $filtered_data = $columns = array();
        // Define field dictionaries
        $columns = $collection == 'users' ?
                // Users collection static schema array
                array('_id', 'ip_address', 'username', 'password', 'salt', 'email', 'activation_code', 'forgotten_password_code', 'forgotten_password_time', 'remember_code', 'created_on', 'last_login', 'active', 'first_name', 'last_name', 'company', 'phone') :
                // Groups collection static schema array
                array('_id', 'name', 'description');

        if (is_array($data)) {
            foreach ($columns as $column) {
                // Skip unavailable fields
                if (array_key_exists($column, $data)) {
                    $filtered_data[$column] = $data[$column];
                }
            }
        }

        return $filtered_data;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Prepares IP address string for database insertion.
     *
     * @return string
     */
    protected function _prepare_ip($ip_address) {
        return $ip_address;
    }

    // ------------------------------------------------------------------------

    /**
     * Helper: Clones "_id" field value in a new "id" property.
     *
     * We need to add an arbitrary "id" field to the resulted
     * object to maintain IonAuth compatibility with both
     * mongodb library and the native database drivers with
     * minimum level of code change.
     *
     * This helper only clones the _id field value, if:
     * 1. _id field is already present in the array.
     * 2. id field is not set already set with any other values.
     *
     * @param  mixed $result Result array or object
     * @return mixed
     */
    public function _clone_mongoid($result) {
        $data = is_object($result) ? (array) $result : $result;

        // It's an array of array, clone mongoid of each one
        if (isset($data[0])) {
            foreach ($data as $key => $value) {
                $data[$key] = $this->_clone_mongoid($value);
            }
        } elseif (!isset($data['id']) && isset($data['_id'])) {
            $data['id'] = $data['_id'];
        }

        return is_object($result) ? (object) $data : $data;
    }

}

// END Ion_auth_mongodb_model Class

/* End of file ion_auth_mongodb_model.php */
/* Location: ./application/modules/auth/models/ion_auth_mongodb_model.php */